/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2008-2011, Salzburg Research. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * - Neither the name of the KiWi Project nor the names of its contributors
 * may be used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * Contributor(s):
 * sschaffe
 *
 * File Info:
 * User: sschaffe
 * Date: 2011/01/06
 * Time: 15:07
 *
 * Project: KiWi2, kiwi-core module
 */

package kiwi.core.services.user;

import kiwi.core.api.persistence.PersistenceService;
import kiwi.core.api.user.UserService;
import kiwi.core.api.user.UserToken;
import kiwi.core.exception.UserExistsException;
import kiwi.core.model.rdf.KiWiResource;
import kiwi.core.model.user.KiWiUser;
import kiwi.core.qualifiers.cache.KiWiCache;
import kiwi.core.qualifiers.user.AnonymousUser;
import kiwi.core.qualifiers.user.CurrentUser;
import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;
import org.slf4j.Logger;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import javax.inject.Named;
import javax.persistence.*;
import java.util.HashMap;
import java.util.Map;

/**
 * Add file description here!
 * <p/>
 * User: sschaffe
 */
@Named("kiwi.core.userService")
@ApplicationScoped
public class UserServiceImpl implements UserService{


    @Inject
    private Logger log;

    @Inject
    private PersistenceService persistenceService;

    @Inject @KiWiCache("user-cache")
    private Cache userCache;

    @Inject @KiWiCache("user-resource-cache")
    private Cache userResourceCache;


    private Map<String,UserToken> userTokens;


    /**
     * Each thread gets its own Transaction for keeping track of all changes happening
     * inside the thread and transaction. We use a ThreadLocal storage to store different instances
     * local to the respective thread.
     */
    private static ThreadLocal<KiWiUser> currentUser = new ThreadLocal<KiWiUser>();


    public UserServiceImpl() {
        userTokens = new HashMap <String,UserToken>();
    }

    /**
     * initialize() initializes the anonymous and admin user
     * if the database has not yet been set up.
     */
    @PostConstruct
    public void initialize() {
        log.debug("Initializing anonymous user.");
        try {
            KiWiUser anonymousUser = createUser("anonymous", null);
        } catch(UserExistsException e) {
            log.warn("Anonymous user already exists.");
        }
        log.debug("Initializing admin.");
        try {
            // admin password must be set lately.
            KiWiUser adminUser = createUser("admin", null);
        } catch(UserExistsException e) {
            log.warn("Admin already exists.");
        }
    }

    /**
     * Return the currently active user. The method tries to determine the current user using the following means:
     * - the user stored in the session, if existent
     * - the user authenticated using HTTP authentication, if existent
     * - the anonymous user
     *
     * @return
     */
    @Override
    @Produces @CurrentUser
    public KiWiUser getCurrentUser() {

        if(currentUser.get() == null) {
            return getAnonymousUser();
        } else {
            return currentUser.get();
        }
    }



    /**
     * Set the current user to the user  passed as argument. The current user should be associated with
     * the current session in a thread local variable that is cleared again when the request finishes
     * (KiWiUserTokenFilter)
     *
     * @param user
     */
    @Override
    public void setCurrentUser(KiWiUser user) {
        currentUser.set(user);
    }

    /**
     * Clear a current user setting for the current thread. Clears the thread local variable set for the
     * currently running thread.
     */
    @Override
    public void clearCurrentUser() {
        currentUser.remove();
    }



    /**
     * Return the anonymous user. If it does not exist yet, it is created in the database and stored.
     *
     * @return
     */
    @Override
    @Produces @AnonymousUser
    public synchronized KiWiUser getAnonymousUser() {
       KiWiUser anonymousUser = getUserByLogin("anonymous");

       if(anonymousUser == null) {
           try {
               anonymousUser = createUser("anonymous", null);
           } catch (UserExistsException e) {
               log.error("Attemting to create anonymous user which already exists ... programming bug!");
           }
       }

        return anonymousUser;
    }


    /**
     * Create a new user token for the user passed as argument. The user token can be used to identify the user
     * throughout a session
     *
     * @param user
     * @return
     */
    @Override
    public UserToken createUserToken(KiWiUser user) {
        UserToken token = new UserToken(user);

        userTokens.put(token.getUuid().toString(),token);

        return token;
    }

    /**
     * Get the user token with the given ID, if it exists. If the user token does not exist, returns null.
     *
     * @param tokenId
     * @return
     */
    @Override
    public UserToken getUserToken(String tokenId) {
        return userTokens.get(tokenId);
    }


    /**
     * Make the user token passed as argument invalid so that it cannot be used to identify a user anymore.
     *
     * @param token
     */
    @Override
    public void invalidateUserToken(UserToken token) {
        userTokens.remove(token.getUuid().toString());
    }

    /**
     * Create a new user with the provided login. The method first
     * checks of a user with this login already exists; if yes, an exception is thrown. The
     * method does not persist the user; this needs to be done by the caller.
     *
     * @param login login of the user to create
     * @return the newly created user.
     */
    @Override
    //@Restrict("#{s:hasRole('admin')}")
    public KiWiUser createUser(String login, String passwordHash) throws UserExistsException {
        return createUser(login, "", login, passwordHash);
    }



    /**
     * Create a new user with the provided login, first name and last name. The method first
     * checks of a user with this login already exists; if yes, an exception is thrown. The
     * method does not persist the user; this needs to be done by the caller.
     *
     * @param login login of the user to create
     * @param firstName first name of the user to create
     * @param lastName last name of the user to create
     * @return the newly created user.
     */
    @Override
    public KiWiUser createUser(final String login, final String firstName, final String lastName, final String passwordHash) throws UserExistsException {

        if(!userExists(login)) {
            KiWiUser u = new KiWiUser(login);
            u.setFirstName(firstName);
            u.setLastName(lastName);
            u.setPasswordHash(passwordHash);

            EntityManager entityManager = persistenceService.getEntityManager();
            EntityTransaction tx = entityManager.getTransaction();

            tx.begin();
            entityManager.persist(u);
            entityManager.flush();
            tx.commit();

            persistenceService.closeEntityManager(entityManager);
            userCache.put(new Element(login, u));

            return u;
        } else {
            throw new UserExistsException("User "+login+" already exists, cannot create!");
        }
    }




    /**
     * Return a user by login. The user is looked up in the database and returned. In case
     * no user with this login is found, this method returns null.
     *
     * @param login the login to look for
     * @return the user with the given login, or null if no such user exists
     * @see kiwi.core.api.user.UserService#getUserByLogin(java.lang.String)
     */
    public KiWiUser getUserByLogin(String login) {
        return getUserByLogin(login, false);
    }

    /**
     * Return a user by login. The user is looked up in the database and returned. In case
     * no user with this login is found, this method returns null. The second parameter
     * specifies whether a failure should be logged or accepted silently. A silent progress
     * may be useful in case we just want to check whether a user exists.
     *
     * @param login the login to look for
     * @param silent fails silently if set to true and logs errors if set to false
     * @return the user with the given login, or null if no such user exists
     * @see kiwi.core.api.user.UserService#getUserByLogin(java.lang.String)
     */
    private KiWiUser getUserByLogin(String login, boolean silent) {

        KiWiUser u = null;

        if(userCache.get(login) != null) {
            u = (KiWiUser) userCache.get(login).getObjectValue();
        } else {
            EntityManager entityManager = persistenceService.getEntityManager();
            EntityTransaction tx = entityManager.getTransaction();

            tx.begin();
            Query q = entityManager.createNamedQuery("currentUserFactory.getUserByLogin");
            q.setParameter("login", login);

            try {
                q.setMaxResults(1);
                u = (KiWiUser) q.getSingleResult();

                userCache.put(new Element(login,u));
            } catch(NonUniqueResultException ex) {
                log.error("More than one user with login {} found", login);
                ex.printStackTrace();
            } catch(NoResultException ex) {
                if(!silent)
                    log.error("KiWiUser {} not found", login);
            }
            tx.commit();
            persistenceService.closeEntityManager(entityManager);
        }
        return u;
    }


    /**
     * Return a user by login. The user is looked up in the database and returned. In case
     * no user with this login is found, this method returns null.
     *
     * @param resource the resource to look for
     * @return the user with the given resource, or null if no such user exists
     * @see kiwi.core.api.user.UserService#getUserByLogin(String)
     */
    @Override
    public KiWiUser getUserByResource(KiWiResource resource) {
        KiWiUser u = null;

        if(userResourceCache.get(resource) != null) {
            u = (KiWiUser) userCache.get(resource).getObjectValue();
        } else {
            EntityManager entityManager = persistenceService.getEntityManager();
            EntityTransaction tx = entityManager.getTransaction();

            tx.begin();
            Query q = entityManager.createNamedQuery("userService.getUserByResource");
            q.setParameter("resource", resource);

            try {
                q.setMaxResults(1);
                u = (KiWiUser) q.getSingleResult();

                userCache.put(new Element(resource,u));
            } catch(NonUniqueResultException ex) {
                log.error("More than one user with resource {} found", resource);
                ex.printStackTrace();
            } catch(NoResultException ex) {
                log.error("KiWiUser {} not found", resource);
            }
            persistenceService.closeEntityManager(entityManager);
        }
        return u;
    }

    /**
     * Check whether a user with the given login already exists. If so,
     * returns true. Otherwise returns false.
     *
     * @param login the login to look for
     * @return true if a user with this login already exists, false otherwise
     * @see kiwi.core.api.user.UserService#userExists(java.lang.String)
     */
    public boolean userExists(String login) {
        return getUserByLogin(login, true) != null;
    }


}
